/*
 * filesystem.c
 *
 * Copyright (C) 2016 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <filesystem.h>
#include <exception.h>
#include <memory.h>
#include <heap.h>
#include <syscalls.h>

static DECLARE_LIST(fs_driver_list);
static DECLARE_LIST(volumes);
static DECLARE_LOCK(fs_driver_list_lock);
static DECLARE_LOCK(volume_list_lock);

static inline int count_delimiters(const char *string)
{
    int count = 0;
    while ((string = strchr(string, PATH_DELIMITER_CHAR)) != NULL) count++;
    return count;
}

mounted_volume_t *get_volume_from_path(const char *path)
{
    mounted_volume_t *volume = NULL;
    list_entry_t *ptr;
    lock_acquire_shared(&volume_list_lock);

    for (ptr = volumes.next; ptr != &volumes; ptr = ptr->next)
    {
        mounted_volume_t *vol = CONTAINER_OF(ptr, mounted_volume_t, list);
        int length = strlen(vol->mountpoint);

        if (strncmp(path, vol->mountpoint, length) == 0
            && (path[length] == '\0' || path[length] == PATH_DELIMITER_CHAR))
        {
            volume = vol;
            break;
        }
    }

    lock_release(&volume_list_lock);
    return volume;
}

void report_filesystem_event(const char *path, dword_t type)
{
    list_entry_t *ptr;
    mounted_volume_t *volume = get_volume_from_path(path);
    int path_length = strlen(path);
    lock_acquire(&volume->event_watch_list_lock);

    for (ptr = volume->event_watch_list.next; ptr != &volume->event_watch_list; ptr = ptr->next)
    {
        event_watch_entry_t *watch = CONTAINER_OF(ptr, event_watch_entry_t, list);
        reference(&watch->directory->header);
        int prefix_length = strlen(watch->directory->global->path);

        if (strncmp(path, watch->directory->global->path, prefix_length) == 0
            && path[prefix_length] == PATH_DELIMITER_CHAR)
        {
            file_event_t *file_event = __builtin_alloca(sizeof(file_event_t) + path_length);
            file_event->type = type;
            strcpy(file_event->filename, path);

            write_pipe(&watch->pipe, &file_event, sizeof(file_event) + path_length + 1);
        }

        dereference(&watch->directory->header);
    }

    lock_release(&volume->event_watch_list_lock);
}

void file_cleanup(file_t *file)
{
    lock_acquire(&file->volume->lock);
    file->volume->driver->unload_file(file);
    lock_release(&file->volume->lock);

    file->volume->open_files--;
}

void file_instance_cleanup(file_instance_t *instance)
{
    file_t *file = instance->global;

    if (instance->watch)
    {
        ASSERT(file->attributes & FILE_ATTR_DIRECTORY);
        ASSERT(instance->watch->directory == instance);

        lock_acquire(&file->volume->event_watch_list_lock);
        list_remove(&instance->watch->list);
        lock_release(&file->volume->event_watch_list_lock);

        pipe_cleanup(&instance->watch->pipe.header);

        free(instance->watch);
        instance->watch = NULL;
    }

    file->volume->driver->close_file(instance);

    if (instance->mode & FILE_MODE_DELETE_ON_CLOSE)
    {
        if (file->volume->driver->delete_file(file->volume, file->path, FALSE) == ERR_SUCCESS)
        {
            file->attributes |= FILE_ATTR_DELETED;
        }
    }

    dereference(&file->header);
}

void register_filesystem_driver(fs_driver_t *driver)
{
    lock_acquire(&fs_driver_list_lock);
    list_append(&fs_driver_list, &driver->list);
    lock_release(&fs_driver_list_lock);
}

bool_t unregister_filesystem_driver(fs_driver_t *driver)
{
    lock_acquire(&fs_driver_list_lock);
    list_remove(&driver->list);
    lock_release(&fs_driver_list_lock);

    return TRUE;
}

dword_t register_mounted_volume(mounted_volume_t *volume)
{
    lock_acquire(&volume_list_lock);

    lock_init(&volume->lock);;
    volume->open_files = 0;
    list_init(&volume->event_watch_list);
    lock_init(&volume->event_watch_list_lock);

    int delimiters = count_delimiters(volume->mountpoint);
    list_entry_t *ptr;

    for (ptr = volumes.next; ptr != &volumes; ptr = ptr->next)
    {
        mounted_volume_t *current = CONTAINER_OF(ptr, mounted_volume_t, list);
        if (delimiters >= count_delimiters(current->mountpoint)) break;
    }

    list_put_before(ptr, &volume->list);

    lock_release(&volume_list_lock);
    return ERR_SUCCESS;
}

dword_t unregister_mounted_volume(mounted_volume_t *volume)
{
    if (volume->open_files > 0) return ERR_BUSY;
    lock_acquire(&volume_list_lock);
    lock_acquire(&volume->lock);

    list_remove(&volume->list);

    lock_release(&volume->lock);
    lock_release(&volume_list_lock);
    return ERR_SUCCESS;
}

dword_t normalize_path(const char *path, char *normalized_path)
{
    static const char *block_dev_prefix = "BlockDevices/";
    static const char *char_dev_prefix = "CharDevices/";

    dword_t ret = ERR_SUCCESS;
    char *path_copy;
    char *endptr;

    if (*path == '#')
    {
        path_copy = malloc(strlen(path + 1) + strlen(block_dev_prefix) + 1);
        strcpy(path_copy, block_dev_prefix);
        strcat(path_copy, path + 1);
    }
    else if (*path == '@')
    {
        path_copy = malloc(strlen(path + 1) + strlen(char_dev_prefix) + 1);
        strcpy(path_copy, char_dev_prefix);
        strcat(path_copy, path + 1);
    }
    else
    {
        path_copy = strdup(path);
    }

    char *token;
    *normalized_path = '\0';

    for (token = strtok_r(path_copy, PATH_DELIMITER_STRING, &endptr);
         token != NULL;
         token = strtok_r(NULL, PATH_DELIMITER_STRING, &endptr))
    {
        if (strcmp(token, ".") == 0) continue;
        if (strcmp(token, "..") == 0)
        {
            char *ptr = strrchr(normalized_path, PATH_DELIMITER_CHAR);
            if (ptr) *ptr = '\0';
        }

        if ((strlen(normalized_path) + strlen(token) + 2) > MAX_PATH)
        {
            ret = ERR_INVALID;
            break;
        }

        if (normalized_path[0]) strcat(normalized_path, PATH_DELIMITER_STRING);
        strcat(normalized_path, token);
    }

    free(path_copy);
    return ret;
}

sysret_t syscall_mount(const char *device, const char *mountpoint, const char *filesystem, dword_t flags)
{
    dword_t ret = ERR_NOTFOUND;
    list_entry_t *i;
    char *safe_device;
    char *safe_mountpoint;
    char *safe_filesystem;

    if (get_previous_mode() == USER_MODE)
    {
        safe_device = copy_user_string(device);
        safe_mountpoint = copy_user_string(mountpoint);
        safe_filesystem = copy_user_string(filesystem);
    }
    else
    {
        safe_device = (char*)device;
        safe_mountpoint = (char*)mountpoint;
        safe_filesystem = (char*)filesystem;
    }

    lock_acquire(&fs_driver_list_lock);

    for (i = fs_driver_list.next; i != &fs_driver_list; i = i->next)
    {
        fs_driver_t *fs = CONTAINER_OF(i, fs_driver_t, list);

        if (filesystem == NULL || strcmp(fs->name, safe_filesystem) == 0)
        {
            ret = fs->mount(safe_device, safe_mountpoint, flags);
            if (ret == ERR_SUCCESS) break;
        }
    }

    lock_release(&fs_driver_list_lock);

    if (get_previous_mode() == USER_MODE)
    {
        free(safe_device);
        free(safe_mountpoint);
        free(safe_filesystem);
    }

    return ret;
}

sysret_t syscall_unmount(const char *mountpoint)
{
    lock_acquire(&volume_list_lock);
    list_entry_t *ptr;
    mounted_volume_t *vol = NULL;

    for (ptr = volumes.next; ptr != &volumes; ptr = ptr->next)
    {
        mounted_volume_t *current = CONTAINER_OF(ptr, mounted_volume_t, list);

        if (strcmp(current->mountpoint, mountpoint) == 0)
        {
            vol = current;
            break;
        }
    }

    lock_release(&volume_list_lock);

    if (vol->open_files) return ERR_BUSY;
    return vol->driver->unmount(vol);
}

dword_t open_file_internal(const char *path, file_instance_t **file_instance, dword_t mode, dword_t attributes)
{
    char normalized_path[MAX_PATH];
    file_t *file = NULL;
    file_instance_t *instance = NULL;

    dword_t ret = normalize_path(path, normalized_path);
    if (ret != ERR_SUCCESS) return ret;

    mounted_volume_t *vol = get_volume_from_path(normalized_path);
    if (vol == NULL) return ERR_NOTFOUND;

    if (reference_by_name(normalized_path, OBJECT_FILE, (object_t**)&file))
    {
        if (file->attributes & FILE_ATTR_DELETED)
        {
            if (!(mode & FILE_MODE_CREATE))
            {
                dereference(&file->header);
                return ERR_NOTFOUND;
            }

            mode |= FILE_MODE_TRUNCATE;
        }

        if (((file->global_mode ^ mode) & (FILE_MODE_SHARE_READ | FILE_MODE_SHARE_WRITE))
            || (!(file->global_mode & FILE_MODE_SHARE_READ) && (mode & FILE_MODE_READ))
            || (!(file->global_mode & FILE_MODE_SHARE_WRITE) && (mode & (FILE_MODE_WRITE | FILE_MODE_TRUNCATE))))
        {
            dereference(&file->header);
            return ERR_FORBIDDEN;
        }
    }
    else
    {
        file = (file_t*)malloc(sizeof(file_t));
        if (file == NULL) return ERR_NOMEMORY;

        init_object(&file->header, normalized_path, OBJECT_FILE);
        file->volume = vol;

        file->path = &file->header.name[strlen(vol->mountpoint)];
        if (*file->path == PATH_DELIMITER_CHAR) file->path++;

        file->global_mode = mode;
        file->attributes = attributes;

        ret = vol->driver->load_file(&file);
        if (ret != ERR_SUCCESS)
        {
            free(file->header.name);
            free(file);
            return ret;
        }

        ret = create_object(&file->header);
        if (ret != ERR_SUCCESS)
        {
            free(file->header.name);
            free(file);
            return ret;
        }
    }

    instance = (file_instance_t*)malloc(sizeof(file_instance_t));
    if (instance == NULL)
    {
        dereference(&file->header);
        return ERR_NOMEMORY;
    }

    init_object(&instance->header, NULL, OBJECT_FILE_INSTANCE);
    instance->global = file;
    instance->mode = mode;

    ret = vol->driver->open_file(&instance);
    if (ret != ERR_SUCCESS)
    {
        free(instance);
        dereference(&file->header);
        return ERR_NOMEMORY;
    }

    ret = create_object(&instance->header);
    if (ret == ERR_SUCCESS)
    {
        *file_instance = instance;
    }
    else
    {
        free(instance);
        dereference(&file->header);
    }

    return ret;
}

sysret_t syscall_open_file(const char *path, handle_t *handle, dword_t mode, dword_t attributes)
{
    dword_t ret = ERR_SUCCESS;
    file_instance_t *file;
    char *safe_path;
    handle_t safe_handle = 0;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(handle, sizeof(handle_t))) return ERR_BADPTR;
        safe_path = copy_user_string(path);
        if (safe_path == NULL)
        {
            ret = ERR_BADPTR;
            goto cleanup;
        }
    }
    else safe_path = (char*)path;

    ret = open_file_internal(safe_path, &file, mode, attributes);
    if (ret != ERR_SUCCESS) goto cleanup;

    ret = open_object(&file->header, 0, &safe_handle);
    if (ret != ERR_SUCCESS)
    {
        dereference(&file->header);
        goto cleanup;
    }

    EH_TRY
    {
        *handle = safe_handle;
    }
    EH_CATCH
    {
        syscall_close_object(safe_handle);
        ret = ERR_BADPTR;
    }
    EH_DONE;

cleanup:
    if (get_previous_mode() == USER_MODE) free(safe_path);
    return ret;
}

sysret_t syscall_delete_file(const char *path)
{
    dword_t ret = ERR_SUCCESS;
    char normalized_path[MAX_PATH];
    char *safe_path;

    if (get_previous_mode() == USER_MODE)
    {
        safe_path = copy_user_string(path);
        if (safe_path == NULL) return ERR_NOMEMORY;
    }
    else
    {
        safe_path = (char*)path;
    }

    ret = normalize_path(safe_path, normalized_path);
    if (ret != ERR_SUCCESS) goto cleanup;

    mounted_volume_t *vol = get_volume_from_path(normalized_path);
    if (vol == NULL) return ERR_NOTFOUND;

    if (vol->flags & MOUNT_FLAG_READONLY)
    {
        ret = ERR_WRITEPROT;
        goto cleanup;
    }

    lock_acquire(&vol->lock);

    file_t *file = NULL;
    reference_by_name(normalized_path, OBJECT_FILE, (object_t**)&file);

    char *relative_path = &normalized_path[strlen(vol->mountpoint)];
    if (*relative_path == PATH_DELIMITER_CHAR) relative_path++;
    ret = vol->driver->delete_file(vol, relative_path, file == NULL);

    if (ret == ERR_SUCCESS && file != NULL)
    {
        file->attributes |= FILE_ATTR_DELETED;
        dereference(&file->header);
    }

    lock_release(&vol->lock);

cleanup:
    if (get_previous_mode() == USER_MODE) free(safe_path);
    return ret;
}

sysret_t syscall_query_file(handle_t handle, file_info_type_t type, void *buffer, size_t size)
{
    dword_t ret = ERR_SUCCESS;
    void *safe_buffer;
    file_instance_t *file = NULL;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(buffer, size)) return ERR_BADPTR;

        safe_buffer = malloc(size);
        if (safe_buffer == NULL) return ERR_NOMEMORY;
        memset(safe_buffer, 0, size);
    }
    else
    {
        safe_buffer = buffer;
    }

    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&file)) return ERR_INVALID;

    lock_acquire_shared(&file->global->volume->lock);

    switch (type)
    {
    case FILE_INFO_ATTRIBUTES:
        if (size >= sizeof(dword_t)) *((dword_t*)safe_buffer) = file->global->attributes;
        else ret = ERR_SMALLBUF;
        break;

    case FILE_INFO_NAME:
        if (size >= (strlen(file->global->header.name) + 1)) strncpy(safe_buffer, file->global->header.name, size);
        else ret = ERR_SMALLBUF;
        break;

    case FILE_INFO_TIME:
        ret = ERR_NOSYSCALL; // TODO
        break;

    case FILE_INFO_SIZE:
        if (size >= sizeof(qword_t)) *((qword_t*)safe_buffer) = file->global->size;
        else ret = ERR_SMALLBUF;
        break;

    case FILE_INFO_OWNER:
        if (size >= sizeof(dword_t)) *((dword_t*)safe_buffer) = file->global->owner_uid;
        else ret = ERR_SMALLBUF;
        break;

    default:
        ret = ERR_INVALID;
    }

    lock_release(&file->global->volume->lock);

    if (get_previous_mode() == USER_MODE)
    {
        EH_TRY memcpy(buffer, safe_buffer, size);
        EH_CATCH ret = ERR_BADPTR;
        EH_DONE;
    }

    if (file) dereference(&file->header);
    if (get_previous_mode() == USER_MODE) free(safe_buffer);
    return ret;
}

sysret_t syscall_set_file(handle_t handle, file_info_type_t set_type, void *buffer, size_t size)
{
    void *safe_buffer;
    file_instance_t *file = NULL;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(buffer, size)) return ERR_BADPTR;

        safe_buffer = malloc(size);
        if (safe_buffer == NULL) return ERR_NOMEMORY;
        memcpy(safe_buffer, buffer, size);
    }
    else
    {
        safe_buffer = buffer;
    }

    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&file)) return ERR_INVALID;

    lock_acquire(&file->global->volume->lock);
    dword_t ret = file->global->volume->driver->set_file(file->global, set_type, safe_buffer, size);
    lock_release(&file->global->volume->lock);

    if (file) dereference(&file->header);
    if (get_previous_mode() == USER_MODE) free(safe_buffer);
    return ret;
}

sysret_t syscall_list_directory(handle_t handle, char *filename, bool_t continue_scan)
{
    file_instance_t *directory;
    char safe_filename[MAX_PATH];

    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&directory)) return ERR_INVALID;

    lock_acquire_shared(&directory->global->volume->lock);
    dword_t ret = directory->global->volume->driver->list_dir(directory, safe_filename, continue_scan);
    lock_release(&directory->global->volume->lock);

    if (ret == ERR_SUCCESS)
    {
        if (get_previous_mode() != USER_MODE || check_usermode(filename, strlen(safe_filename) + 1))
        {
            EH_TRY strcpy(filename, safe_filename);
            EH_CATCH ret = ERR_BADPTR;
            EH_DONE;
        }
        else
        {
            ret = ERR_BADPTR;
        }
    }

    return ret;
}

sysret_t syscall_read_file(handle_t handle, void *buffer, qword_t offset, size_t size, size_t *bytes_read)
{
    dword_t ret;
    file_instance_t *file;
    dword_t read_count = 0;
    void *safe_buffer = NULL;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(buffer, size)) return ERR_BADPTR;
        if (!check_usermode(bytes_read, sizeof(dword_t))) return ERR_BADPTR;

        ret = memory_pin_buffer(buffer, &safe_buffer, size, FALSE);
        if (ret != ERR_SUCCESS) return ret;
    }
    else
    {
        safe_buffer = buffer;
    }

    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&file)) return ERR_INVALID;

    if (!(file->mode & FILE_MODE_READ))
    {
        ret = ERR_FORBIDDEN;
        goto cleanup;
    }

    lock_acquire_shared(&file->global->volume->lock);
    ret = file->global->volume->driver->read_file(file, safe_buffer, offset, size, &read_count);
    lock_release(&file->global->volume->lock);

cleanup:
    EH_TRY *bytes_read = read_count;
    EH_CATCH ret = ERR_BADPTR;
    EH_DONE;

    if (get_previous_mode() == USER_MODE) memory_free(memory_upper_space, safe_buffer);
    dereference(&file->header);
    return ret;
}

sysret_t syscall_write_file(handle_t handle, const void *buffer, qword_t offset, size_t size, dword_t *bytes_written)
{
    dword_t ret;
    file_instance_t *file;
    dword_t written_count = 0;
    void *safe_buffer = NULL;

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode(buffer, size)) return ERR_BADPTR;
        if (!check_usermode(bytes_written, sizeof(dword_t))) return ERR_BADPTR;

        ret = memory_pin_buffer(buffer, &safe_buffer, size, TRUE);
        if (ret != ERR_SUCCESS) return ret;
    }
    else
    {
        safe_buffer = (void*)buffer;
    }

    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&file)) return ERR_INVALID;

    if (!(file->mode & FILE_MODE_WRITE))
    {
        ret = ERR_FORBIDDEN;
        goto cleanup;
    }

    if (file->global->volume->flags & MOUNT_FLAG_READONLY)
    {
        ret = ERR_WRITEPROT;
        goto cleanup;
    }

    lock_acquire(&file->global->volume->lock);
    ret = file->global->volume->driver->write_file(file, safe_buffer, offset, size, &written_count);
    lock_release(&file->global->volume->lock);

    EH_TRY *bytes_written = written_count;
    EH_DONE;

cleanup:
    if (get_previous_mode() == USER_MODE) memory_free(memory_upper_space, safe_buffer);
    dereference(&file->header);
    return ret;
}

sysret_t syscall_wait_directory_event(handle_t handle, dword_t event_mask, file_event_t *buffer, size_t size, dword_t timeout)
{
    file_instance_t *directory;

    if (get_previous_mode() == USER_MODE && !check_usermode(buffer, size)) return ERR_BADPTR;
    if (!reference_by_handle(handle, OBJECT_FILE_INSTANCE, (object_t**)&directory)) return ERR_INVALID;

    if (!(directory->global->attributes & FILE_ATTR_DIRECTORY))
    {
        dereference(&directory->header);
        return ERR_ISNOTDIR;
    }

    if (directory->watch == NULL)
    {
        event_watch_entry_t *watch = (event_watch_entry_t*)malloc(sizeof(event_watch_entry_t));
        watch->directory = directory;
        init_pipe(&watch->pipe, PIPE_MESSAGE);

        mounted_volume_t *volume = directory->global->volume;
        lock_acquire(&volume->event_watch_list_lock);
        list_append(&volume->event_watch_list, &watch->list);

        if (!__sync_bool_compare_and_swap(&directory->watch, NULL, watch))
        {
            list_remove(&watch->list);
            free(watch);
        }

        lock_release(&volume->event_watch_list_lock);
    }

    directory->watch->event_mask = event_mask;
    dword_t ret = read_pipe(&directory->watch->pipe, buffer, &size, timeout);

    dereference(&directory->header);
    return ret;
}
